package academic.emailServer;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.UUID;
import java.util.logging.Logger;


import academic.emailClient.CommunicationInstruction;
import academic.emailClient.model.entities.Meeting;
import academic.emailClient.model.entities.MeetingMerger;
import academic.emailClient.model.entities.Message;

public class EmailServer {
	
	//Host where server is being executed
	private String host;
	//Port where serve is being accessed
	private int port;
	//List of clients that are connected to server
	private HashMap<String, EmailClientThread> emailClients;
	//List of messages to be delivered
	private HashMap<String, ArrayList<Message>> pendingMessages;
	static Logger logger = Logger.getLogger(EmailServer.class.getClass().getName());
	//Meetings created
	private HashMap<String, ArrayList<Meeting>> pendingMeetings;
	private HashMap<UUID, Meeting> meetings;
		

	/**
	 * Email Server constructor
	 * @param host Host where server is being executed
	 * @param port Port where server is being accessed
	 */
	public EmailServer(String host, int port) {
		super();
		this.host = host;
		this.port = port;
		this.emailClients = new HashMap<String, EmailServer.EmailClientThread>();
		this.pendingMessages = new HashMap<String, ArrayList<Message>>();
		this.pendingMeetings = new HashMap<String, ArrayList<Meeting>>();	
		this.meetings = new HashMap<UUID, Meeting>();
	}


	/**
	 * Start server execution
	 */
	public void start() {
		boolean listening = true;		
		
		try {					
			ServerSocket server = new ServerSocket(port);
			while (listening){
				Socket client = server.accept();
				EmailClientThread clientThread = new EmailClientThread(client);
				clientThread.start();
			}
			
			//Closes server
			server.close();
			for (EmailClientThread clientThread : emailClients.values()){
				clientThread.close();
			}
		}
		catch (Exception e){
			logger.severe(e.toString());
		}
	}
	
	/**
	 * Sends a message to the different receivers of the mail
	 * @param message Message to be sent
	 */
	private synchronized void sendMessage(Message message){
		String[] receivers = message.getTo().split(";");
		
		//Goes around all receivers to send them the message received
		for (String receiver : receivers){
			
			logger.info("Receiver: " + receiver);
			
			//Validates if receiver is connected
			if (!emailClients.containsKey(receiver)){
				logger.info("Receiver is not connected");
				//Adds message to pending list, so it can be sent when the receiver is connected
				if (!pendingMessages.containsKey(receiver))
					pendingMessages.put(receiver, new ArrayList<Message>());
				
				logger.info("Adding pending message: " + message.getId().toString());
				ArrayList<Message> messages = pendingMessages.get(receiver);
				messages.add(message);
			}
			//Sends the received message
			else {
				logger.info("Receiver is connected");
				emailClients.get(receiver).writeMessage(message);
			}
		}
	}
	
	/**
	 * Sends a Meeting to the different attendees of the meeting
	 * @param meeting Meeting to be sent
	 */
	private synchronized void sendMeeting(Meeting meeting){
		
		//Adds meeting in the meetings list
		meetings.put(meeting.getId(), meeting);																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																											
		
		//Goes around all receivers to send them the message received
		for (String attendee : meeting.getAttendeesEmails()){
			
			logger.info("Attendee: " + attendee);
			
			//Validates if attendee is connected
			if (!emailClients.containsKey(attendee)){
				logger.info("Attendee is not connected");
				//Adds meeting to pending list, so it can be sent when the receiver is connected
				if (!pendingMeetings.containsKey(attendee))
					pendingMeetings.put(attendee, new ArrayList<Meeting>());
					
				ArrayList<Meeting> meetings = pendingMeetings.get(attendee);
				meetings.add(meeting);
			}
			//Sends the attendee message
			else {
				logger.info("Attendee is connected");
				emailClients.get(attendee).writeMeeting(meeting);
			}
		}
	}
	
	
	
	/**
	 * Saves a meeting verifying that it has not been modified by someone else, 
	 * if this happens, conflicts have to be resolved
	 * @param meeting, meeting that is going to be saved and is going to replace previous version
	 * @param modifierAccount User account that has made the change
	 */
	private synchronized void saveMeeting(Meeting toSaveMeeting, String modifierAccount){
		EmailClientThread modifier = emailClients.get(modifierAccount);
				
		Meeting previousMeeting = meetings.get(toSaveMeeting.getId());
		MeetingMerger merger = new MeetingMerger(previousMeeting, toSaveMeeting, meetings);	
		
		logger.info("Original Meeting " + previousMeeting.getId().toString() + " V." + previousMeeting.getVersion());
		logger.info("Updated Meeting " + toSaveMeeting.getId().toString() + " V." + toSaveMeeting.getVersion());
		logger.info("Location " + previousMeeting.getLocation() + " " + toSaveMeeting.getLocation());
		logger.info("Purpose " + previousMeeting.getPurpose() + " " + toSaveMeeting.getPurpose());
		logger.info("Description " + previousMeeting.getDescription() + " " + toSaveMeeting.getDescription());
		//Validates versions
		if (previousMeeting.getVersion() == toSaveMeeting.getVersion()){
			logger.info("Meeting has the same version than the one in server");
			//Merge dates validating others
			merger.mergeMeetingSchedule();
			toSaveMeeting = merger.getMergeResult();
			sendUpdatedMeeting(toSaveMeeting, modifier);
		}
		else{
			//TODO Resolve conflicts with optimistic lock	
			logger.info("Meeting has different Versions and has to merge results");
			toSaveMeeting = merger.mergeMeetings();
			
			
			if (merger.mergeHasConflicts()){
				logger.info("Meeting has conflicts that weren't resolved");
				modifier.resolveMeetingConflicts(previousMeeting, toSaveMeeting, merger.getConflictsList());
			}
			else {
				toSaveMeeting.setVersion(previousMeeting.getVersion());
				logger.info("Meeting had differences but were resolved");
				sendUpdatedMeeting(toSaveMeeting, modifier);
			}
		}								
		
	}
	
	
	/**
	 * Sends an update to all clients that are involved with the meeting
	 * @param updatedMeeting Meeting that has been modified and accepted
	 * @param modifier Account of the user responsible for the update
	 */
	private synchronized void sendUpdatedMeeting(Meeting updatedMeeting, EmailClientThread modifier){		
		updatedMeeting.upgradeVersion();
		//toSaveMeeting.setAttendees(previousMeeting.getAttendees());			
		meetings.put(updatedMeeting.getId(), updatedMeeting);
		
		//Must send meeting to all users
		//Updates wasn't made by the owner, so the owner needs to receive modifications
		if (!updatedMeeting.getOwner().equals(modifier.emailAccount)){
			EmailClientThread owner = emailClients.get(updatedMeeting.getOwner()); 
			owner.sendMeetingUpdate(updatedMeeting);
		}
		
		//Sends update to all attendees except the one that modified the owner
		for (String attendeeAccount : updatedMeeting.getAttendeesEmails()){
			//Validates the attendee to not send the update
			if (attendeeAccount.equals(modifier.emailAccount))
				continue;
			
			EmailClientThread attendee = emailClients.get(attendeeAccount);
			attendee.sendMeetingUpdate(updatedMeeting);
		}
		
		//Confirm update to the modifier			
		modifier.confirmMeetingUpdate(updatedMeeting);
	}
	
	/**
	 * Send declination of a meeting from one of the attendees
	 * @param meeting
	 * @param decliner
	 */
	private synchronized void sendMeetingDeclination(UUID meetingID, String decliner){
		if (!meetings.containsKey(meetingID))
			return;
		
		Meeting meeting = meetings.get(meetingID);
		logger.info("Removing attendee from meeting " + decliner);
		meeting.removeAttendeeByAccount(decliner);
		
		EmailClientThread owner = emailClients.get(meeting.getOwner());
		owner.sendDeclination(meeting.getId(), decliner);
		
		for (String attendeeAccount : meeting.getAttendeesEmails()){
			EmailClientThread attendee = emailClients.get(attendeeAccount);
			attendee.sendDeclination(meeting.getId(), decliner);
		}
	}
	
	private synchronized void cancelMeeting(UUID meetingID){
		if (!meetings.containsKey(meetingID))
			return;
		
		Meeting toDeleteMeeting = meetings.get(meetingID);
		for (String attendeeAccount : toDeleteMeeting.getAttendeesEmails()){
			EmailClientThread attendee = emailClients.get(attendeeAccount);
			attendee.sendMeetingCancellation(meetingID);			
		}
		meetings.remove(toDeleteMeeting);
	}
	
	/**
	 * Executable method that instantiates the server and initializes it
	 * @param args
	 */
	public static void main (String[] args){
		int port = 8082;
		EmailServer server = new EmailServer("localhost", port);
		server.start();
	}
	
	/**
	 * Class that implements the email client communication channel
	 * @author Alejandra
	 *
	 */
	class EmailClientThread extends Thread {
		private Socket client;
		private ObjectInputStream in;
		private ObjectOutputStream out;
		private String emailAccount;

		/**
		 * Constructor
		 * @param client Socket that communicates server and client
		 */
		public EmailClientThread(Socket client) {
			super();
			this.client = client;
			
			try {
				logger.info("Connected....");				
				out = new ObjectOutputStream(client.getOutputStream());
				in = new ObjectInputStream(client.getInputStream());				
				emailAccount = (String) in.readObject();
				emailClients.put(emailAccount, this);
				logger.info("Email Account: " + emailAccount);
			}
			catch (IOException | ClassNotFoundException e){
				logger.severe(e.toString());
				close();
			}
		}

		/**
		 * Implements the server protocol
		 */
		public void run(){
			Integer instructionCode;
			CommunicationInstruction instruction;
			
			try {
				logger.info("Waiting for instruction...");
				sendPendingMessages();
				sendPendingMeetings();
	
				while ((instructionCode = (Integer) in.readObject()) != null) {
					try {
						instruction = CommunicationInstruction.convert(instructionCode);
						logger.info("Exectuting instruction " + instruction.name());
						switch (instruction){

						case SEND_MESSAGE:
							Message message = (Message) in.readObject();
							logger.info("Read Message: " + message.getId().toString());
							sendMessage(message);
							break;

						case CREATE_MEETING:
							Meeting meeting = (Meeting) in.readObject();
							logger.info("Read Meeting: " + meeting.getId().toString());
							sendMeeting(meeting);
							break;
						
						case DECLINE_MEETING:
							UUID meetingID = (UUID) in.readObject();
							String decliner = (String) in.readObject();
							sendMeetingDeclination(meetingID, decliner);
							break;
						case CANCEL_MEETING:
							UUID toCancelMeeting = (UUID) in.readObject();
							cancelMeeting(toCancelMeeting);
							break;
						case REQUEST_UPDATE_MEETING:
							Meeting toSaveMeeting = (Meeting) in.readObject();
							saveMeeting(toSaveMeeting, emailAccount);
						default:
							break;
						}
					}
					catch (IOException ioe){
						ioe.printStackTrace();
						logger.severe(ioe.toString());
						break;
					}
				}
				close();
			} 
			catch(Exception e) {
				close();
				logger.severe(e.toString());
				e.printStackTrace();
			}
		}
		
		/**
		 * Writes a new message to the client
		 * @param message Message object
		 */
		private void writeMessage(Message message){
			try {
				logger.info("Sending message to " + emailAccount);
				if (!client.isConnected()){
					logger.info("Client is not connected");
					emailClients.remove(emailAccount);
					return;
				}
				
				out.writeObject(CommunicationInstruction.RECEIVE_MESSAGE.value());
				out.writeObject(message);
			}
			catch (IOException e){
				logger.severe(e.toString());
			}
		}
			
		/**
		 * Writes a meeting to the client
		 * @param Meeting created meeting
		 */
		private void writeMeeting(Meeting meeting){
			try {
				logger.info("Sending meeting to " + emailAccount);
				if (!client.isConnected()){
					logger.info("Client is not connected");
					emailClients.remove(emailAccount);
					return;
				}
				
				out.writeObject(CommunicationInstruction.CREATE_MEETING.value());
				out.writeObject(meeting);
			}
			catch (IOException e){
				logger.severe(e.toString());
			}
		}
		
		/**
		 * Sends messages that are pending to send because the client wasn't connected
		 */
		@SuppressWarnings("unchecked")
		private synchronized void sendPendingMessages(){
			//Validates if client has been has pending messages to receive
			if (!pendingMessages.containsKey(emailAccount))
				return;
						
			//Validates if client has pending messages
			if (pendingMessages.get(emailAccount).size() == 0)
				return;
				
			//Sends pending messages to client and removes it from the list
			ArrayList<Message> readingMessages = (ArrayList<Message>) pendingMessages.get(emailAccount).clone();
			for (Message message : readingMessages){
				logger.info("Pending message: " + message.getId().toString());
				writeMessage(message);
				pendingMessages.get(emailAccount).remove(message);					
			}
		}
		
		
		/**
		 * Sends meetings that are pending to be delivered
		 */
		@SuppressWarnings("unchecked")
		private synchronized void sendPendingMeetings(){
			//Validates if client has been added to pending meetings to receive
			if (!pendingMeetings.containsKey(emailAccount))
				return;
			
			//Validates if client has pending messages
			if (pendingMeetings.get(emailAccount).size() == 0)
				return;
			
			//Sends pending meetings
			ArrayList<Meeting> readingMeetings = (ArrayList<Meeting>) pendingMeetings.get(emailAccount).clone();
			for (Meeting meeting : readingMeetings){
				writeMeeting(meeting);
				pendingMeetings.get(emailAccount).remove(meeting);
			}
		}
			
		/**
		 * Notifies to the client about a meeting declination, some attendee decided not to go
		 * @param meetingID ID Client must be invited to the meeting, to be able to modify it
		 * @param attendee Attendee should be invited to the meeting
		 */
		private void sendDeclination(UUID meetingID, String decliner){
			try {
				logger.info("Sending declination to " + emailAccount);
				if (!client.isConnected()){
					logger.info("Client is not connected");
					emailClients.remove(emailAccount);
					return;
				}
				
				out.writeObject(CommunicationInstruction.RECEIVE_MEETING_DECLINATION.value());
				out.writeObject(meetingID);
				out.writeObject(decliner);
			}
			catch (IOException e){
				logger.severe(e.toString());
			}
		}
		
		/**
		 * 
		 * @param meetingID
		 */
		public void sendMeetingCancellation(UUID meetingID) {
			try {
				logger.info("Sending cancellation to " + emailAccount);
				if (!client.isConnected()){
					logger.info("Client is not connected");
					emailClients.remove(emailAccount);
					return;
				}
				
				out.writeObject(CommunicationInstruction.CANCEL_MEETING.value());
				out.writeObject(meetingID);
			}
			catch (IOException e){
				logger.severe(e.toString());
			}
		}
		
		/**
		 * Sends to the user the meeting updated by other user
		 * @param meeting
		 */
		public void sendMeetingUpdate(Meeting meeting){
			try {
				logger.info("Sending Update to " + emailAccount);
				if (!client.isConnected()){
					logger.info("Client is not connected");
					emailClients.remove(emailAccount);
					return;
				}
				
				out.writeObject(CommunicationInstruction.RECEIVE_MEETING_UPDATE.value());
				out.writeObject(meeting);
			}
			catch (IOException e){
				logger.severe(e.toString());
			}	
		}
		
		/**
		 * Sends update confirmation to the user that requested the meeting update
		 * @param meeting Meeting that has been accepted and updated in the server
		 */
		public void confirmMeetingUpdate(Meeting meeting){
			try {
				logger.info("Sending confirmation of meeting update to " + emailAccount);
				if (!client.isConnected()){
					logger.info("Client is not connected");
					emailClients.remove(emailAccount);
					return;
				}
				
				out.writeObject(CommunicationInstruction.CONFIRM_MEETING_UPDATE.value());
				out.writeObject(meeting);
			}
			catch (IOException e){
				logger.severe(e.toString());
			}
		}
		
		public void resolveMeetingConflicts(Meeting previousMeeting, Meeting meetingWithConflicts, ArrayList<String> conflicts){
			try {
				logger.info("Sending request for solving conflicts to  " + emailAccount);
				if (!client.isConnected()){
					logger.info("Client is not connected");
					emailClients.remove(emailAccount);
					return;
				}
				
				out.writeObject(CommunicationInstruction.RESOLVE_MEETING_CONFLICTS.value());
				out.writeObject(previousMeeting);
				out.writeObject(meetingWithConflicts);
				out.writeObject(conflicts);
			}
			catch (IOException e){
				logger.severe(e.toString());
			}
		}
		
		/**
		 * Closes the client socket and the streams related with it
		 */
		private void close(){
			
			//Closes output stream
			try{
				if (out != null)
					out.close();
			}
			catch (Exception e) {
				logger.severe(e.toString());
			}	
			
			//Closes input stream
			try {
				if (in != null)
					in.close();
			}
			catch (Exception e){	
				logger.severe(e.toString());
			}
			
			//Closes socket and remove client from the list of email clients
			try {
				if (emailClients.containsKey(emailAccount))
					emailClients.remove(emailAccount);
				
				if (client != null)
					client.close();									
			}
			catch (Exception e){
				logger.severe(e.toString());
			}
		}
	}

}
